/**
 * ***************************************************************************** Copyright (c) 2005,
 * 2011 IBM Corporation and others. All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0 which accompanies this
 * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: IBM Corporation - initial API and implementation Sergey Prigogin
 * <eclipse.sprigogin@gmail.com> - [refactoring] Provide a way to implement refactorings that depend
 * on resources that have to be explicitly released - https://bugs.eclipse.org/347599
 * *****************************************************************************
 */
package org.eclipse.ltk.core.refactoring;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.ltk.core.refactoring.history.IRefactoringExecutionListener;
import org.eclipse.ltk.core.refactoring.history.IRefactoringHistoryListener;
import org.eclipse.ltk.core.refactoring.history.IRefactoringHistoryService;

/**
 * Descriptor object of a refactoring.
 *
 * <p>A refactoring descriptor contains refactoring-specific data which allows the framework to
 * completely reconstruct a particular refactoring instance and execute it on an arbitrary
 * workspace.
 *
 * <p>Refactoring descriptors contain the following information:
 *
 * <ul>
 *   <li>a short description string, which provides a human-readable text designed to be displayed
 *       in the user interface to represent the refactoring in trees and lists. Descriptions are
 *       automatically generated by refactorings.
 *   <li>an optional comment string, which provides a full human-readable description of the
 *       refactoring. Comments are automatically generated by refactorings and provide more
 *       refactoring-specific information, such as which elements have participated in the
 *       refactoring.
 *   <li>refactoring descriptor flags, which tell the framework what capabilities or properties a
 *       certain refactorings has when executed in a remote context.
 *   <li>a timestamp, measured as the milliseconds since January 1, 1970, 00:00:00 GMT, which
 *       denotes the original execution time of the refactoring.
 *   <li>a unique ID, which denotes a certain kind of refactoring (e.g. Rename File). This ID is
 *       usually composed of the plugin identifier of the contributing plugin and a plugin-wide
 *       unique identifier (e.g. <code>org.eclipse.ltk.core.refactoring.renameFile</code>).
 *   <li>the optional name of the project this refactoring is associated with. Note that the project
 *       name is not available if the refactoring cannot be associated with a single project, or the
 *       refactoring descriptor has been read from a file which cannot be associated with a project.
 * </ul>
 *
 * <p>Refactoring descriptors are identified by their refactoring id {@link #getID()} and their time
 * stamps {@link #getTimeStamp()} and are potentially heavy weight objects which should not be held
 * on to. Use refactoring descriptor proxies {@link RefactoringDescriptorProxy} to present
 * refactoring descriptors in the user interface or otherwise manipulate refactoring histories.
 *
 * <p>Clients which create specific refactoring descriptors during change generation should choose a
 * short, informative and human-readable description of the particular refactoring instance and pass
 * appropriate descriptor flags to the constructor. More details about a particular refactoring can
 * be revealed in the comment, which contains more text with refactoring-specific information.
 *
 * <p>Refactoring descriptors do not provide version information. It is the responsibility of the
 * client to enhance subclasses with refactoring version information in order to provide a means of
 * schema evolution.
 *
 * <p>All time stamps are measured as the milliseconds since January 1, 1970, 00:00:00 GMT.
 *
 * <p>Note: this class is indented to be subclassed by clients to provide specialized refactoring
 * descriptors for particular refactorings.
 *
 * @see RefactoringDescriptorProxy
 * @see IRefactoringHistoryService
 * @since 3.2
 */
public abstract class RefactoringDescriptor implements Comparable {

  /**
   * Constant describing the API change flag (value: <code>1</code>).
   *
   * <p>Clients should set this flag to indicate that the represented refactoring may cause breaking
   * API changes. If clients set the {@link #BREAKING_CHANGE} flag, they should set {@link
   * #STRUCTURAL_CHANGE} as well. Typically, refactorings which change elements that are marked as
   * API according to the semantics of the associated programming language should set this flag.
   * This flag is used by the refactoring framework to determine whether a refactoring may break
   * existing API when replayed by clients.
   */
  public static final int BREAKING_CHANGE = 1 << 0;

  /**
   * The unknown refactoring id (value: <code>org.eclipse.ltk.core.refactoring.unknown</code>).
   *
   * <p>This id is reserved by the refactoring framework to signal that a refactoring has been
   * performed which did not deliver a refactoring descriptor via its {@link Change#getDescriptor()}
   * method. The refactoring history service never returns unknown refactorings. For consistency
   * reasons, they are reported for {@link IRefactoringExecutionListener} or {@link
   * IRefactoringHistoryListener} in order to keep clients of these listeners synchronized with the
   * workbench's operation history.
   */
  public static final String ID_UNKNOWN = "org.eclipse.ltk.core.refactoring.unknown"; // $NON-NLS-1$

  /**
   * Constant describing the multi change flag (value: <code>4</code>).
   *
   * <p>Clients should set this flag to indicate that the change created by the represented
   * refactoring might causes changes in other files than the files of the input elements according
   * to the semantics of the associated programming language. Typically, refactorings which update
   * references to the refactored element should set this flag. This flag is used during team
   * synchronize operations to optimize the processing of refactorings.
   */
  public static final int MULTI_CHANGE = 1 << 2;

  /** Constant describing the absence of any flags (value: <code>0</code>). */
  public static final int NONE = 0;

  /**
   * Constant describing the structural change flag (value: <code>2</code>).
   *
   * <p>Clients should set this flag to indicate that the change created by the represented
   * refactoring might be a structural change according to the semantics of the associated
   * programming language. Typically, refactorings which cause changes in elements other than the
   * element which declares the refactored element should set this flag. This flag is used by
   * language-specific tools to determine whether the refactoring may impact client code.
   */
  public static final int STRUCTURAL_CHANGE = 1 << 1;

  /**
   * Constant describing the user flag (value: <code>256</code>).
   *
   * <p>This constant is not intended to be used in refactoring descriptors. Clients should use the
   * value of this constant to define user-defined flags with integer values greater than this
   * constant. Clients must not use this constant directly.
   */
  public static final int USER_CHANGE = 1 << 8;

  /** The comment of the refactoring, or <code>null</code> for no comment */
  private String fComment;

  /** The non-empty description of the refactoring */
  private String fDescription;

  /** The flags of the refactoring, or <code>NONE</code> */
  private int fFlags;

  /** The project name, or <code>null</code> for no project */
  private String fProject;

  /** The unique id of the refactoring */
  private final String fRefactoringId;

  /**
   * The time stamp, or <code>-1</code> if no time information is associated with the refactoring
   */
  private long fTimeStamp = -1;

  /**
   * Creates a new refactoring descriptor.
   *
   * @param id the unique id of the refactoring
   * @param project the non-empty name of the project associated with this refactoring, or <code>
   *     null</code> for a workspace refactoring
   * @param description a non-empty human-readable description of the particular refactoring
   *     instance
   * @param comment the human-readable comment of the particular refactoring instance, or <code>null
   *     </code> for no comment
   * @param flags the flags of the refactoring descriptor
   */
  protected RefactoringDescriptor(
      final String id,
      final String project,
      final String description,
      final String comment,
      final int flags) {
    Assert.isNotNull(id);
    Assert.isLegal(!"".equals(id), "Refactoring id must not be empty"); // $NON-NLS-1$ //$NON-NLS-2$
    Assert.isLegal(
        project == null || !"".equals(project),
        "Project must either be null or non-empty"); // $NON-NLS-1$ //$NON-NLS-2$
    Assert.isNotNull(description);
    Assert.isLegal(
        !"".equals(description), "Description must not be empty"); // $NON-NLS-1$//$NON-NLS-2$
    Assert.isLegal(flags >= NONE, "Flags must be non-negative"); // $NON-NLS-1$
    fRefactoringId = id;
    fDescription = description;
    fProject = project;
    fComment = comment;
    fFlags = flags;
  }

  /** {@inheritDoc} */
  public final int compareTo(final Object object) {
    if (object instanceof RefactoringDescriptor) {
      final RefactoringDescriptor descriptor = (RefactoringDescriptor) object;
      final long delta = fTimeStamp - descriptor.fTimeStamp;
      if (delta < 0) return -1;
      else if (delta > 0) return +1;
    }
    return 0;
  }

  /**
   * Creates the a new refactoring instance for this refactoring descriptor.
   *
   * <p>The returned refactoring must be in an initialized state, i.e. ready to be executed via
   * {@link PerformRefactoringOperation}.
   *
   * <p>This method is not intended to be called directly from code that does not belong to this
   * class and its subclasses. External code should call {@link
   * #createRefactoringContext(RefactoringStatus)} and obtain the refactoring object from the
   * refactoring context.
   *
   * @param status a refactoring status used to describe the outcome of the initialization
   * @return the refactoring, or <code>null</code> if this refactoring descriptor represents the
   *     unknown refactoring, or if no refactoring contribution is available for this refactoring
   *     descriptor which is capable to create a refactoring
   * @throws CoreException if an error occurs while creating the refactoring instance
   */
  public abstract Refactoring createRefactoring(RefactoringStatus status) throws CoreException;

  /**
   * Creates the a new refactoring context and the associated refactoring instance for this
   * refactoring descriptor.
   *
   * <p>This method is used by the refactoring framework to instantiate a refactoring from a
   * refactoring descriptor, in order to apply it later on a local or remote workspace.
   *
   * <p>The default implementation of this method wraps the refactoring in a trivial refactoring
   * context. Subclasses may override this method to create a custom refactoring context.
   *
   * @param status a refactoring status used to describe the outcome of the initialization
   * @return the refactoring context, or <code>null</code> if this refactoring descriptor represents
   *     the unknown refactoring, or if no refactoring contribution is available for this
   *     refactoring descriptor which is capable to create a refactoring.
   * @throws CoreException if an error occurs while creating the refactoring context
   * @since 3.6
   */
  public RefactoringContext createRefactoringContext(RefactoringStatus status)
      throws CoreException {
    Refactoring refactoring = createRefactoring(status);
    if (refactoring == null) return null;
    return new RefactoringContext(refactoring);
  }

  /** {@inheritDoc} */
  public final boolean equals(final Object object) {
    if (object instanceof RefactoringDescriptor) {
      final RefactoringDescriptor descriptor = (RefactoringDescriptor) object;
      return fTimeStamp == descriptor.fTimeStamp
          && getDescription().equals(descriptor.getDescription());
    }
    return false;
  }

  /**
   * Returns the details comment.
   *
   * <p>This information is used in the user interface to show additional details about the
   * performed refactoring.
   *
   * @return the details comment, or the empty string
   */
  public final String getComment() {
    return (fComment != null) ? fComment : ""; // $NON-NLS-1$
  }

  /**
   * Returns the description.
   *
   * <p>This information is used to label a refactoring in the user interface.
   *
   * @return the description
   */
  public final String getDescription() {
    return fDescription;
  }

  /**
   * Returns the flags.
   *
   * @return the flags
   */
  public final int getFlags() {
    return fFlags;
  }

  /**
   * Returns the refactoring id.
   *
   * @return the refactoring id.
   */
  public final String getID() {
    return fRefactoringId;
  }

  /**
   * Returns the project name.
   *
   * @return the non-empty name of the project, or <code>null</code>
   */
  public final String getProject() {
    return fProject;
  }

  /**
   * Returns the time stamp.
   *
   * @return the time stamp, or <code>-1</code> if no time information is available
   */
  public final long getTimeStamp() {
    return fTimeStamp;
  }

  /** {@inheritDoc} */
  public final int hashCode() {
    int code = getDescription().hashCode();
    if (fTimeStamp >= 0) code += (17 * fTimeStamp);
    return code;
  }

  /**
   * Sets the details comment of this refactoring.
   *
   * <p>Note: This API must not be extended or reimplemented and should not be called from outside
   * the refactoring framework.
   *
   * @param comment the comment to set, or <code>null</code> for no comment
   */
  public void setComment(final String comment) {
    fComment = comment;
  }

  /**
   * Sets the description of this refactoring.
   *
   * <p>Note: This API must not be extended or reimplemented and should not be called from outside
   * the refactoring framework.
   *
   * @param description the non-empty description of the refactoring to set
   * @since 3.3
   */
  public void setDescription(final String description) {
    Assert.isNotNull(description);
    Assert.isLegal(
        !"".equals(description), "Description must not be empty"); // $NON-NLS-1$ //$NON-NLS-2$
    fDescription = description;
  }

  /**
   * Sets the flags of this refactoring.
   *
   * <p>Note: This API must not be extended or reimplemented and should not be called from outside
   * the refactoring framework.
   *
   * @param flags the flags to set, or <code>NONE</code> to clear the flags
   * @since 3.3
   */
  public void setFlags(final int flags) {
    Assert.isLegal(flags >= NONE, "Flags must be non-negative"); // $NON-NLS-1$
    fFlags = flags;
  }

  /**
   * Sets the project name of this refactoring.
   *
   * <p>Note: This API must not be extended or reimplemented and should not be called from outside
   * the refactoring framework.
   *
   * @param project the non-empty project name to set, or <code>null</code> for the workspace
   */
  public void setProject(final String project) {
    Assert.isLegal(
        project == null || !"".equals(project),
        "Project must either be null or non-empty"); // $NON-NLS-1$ //$NON-NLS-2$
    fProject = project;
  }

  /**
   * Sets the time stamp of this refactoring. This method can be called only once.
   *
   * <p>Note: This API must not be extended or reimplemented and should not be called from outside
   * the refactoring framework.
   *
   * @param stamp the time stamp to set
   */
  public void setTimeStamp(final long stamp) {
    Assert.isTrue(stamp >= 0);
    fTimeStamp = stamp;
  }

  /** {@inheritDoc} */
  public String toString() {

    final StringBuffer buffer = new StringBuffer(128);

    buffer.append(getClass().getName());
    if (fRefactoringId.equals(ID_UNKNOWN)) buffer.append("[unknown refactoring]"); // $NON-NLS-1$
    else {
      buffer.append("[timeStamp="); // $NON-NLS-1$
      buffer.append(fTimeStamp);
      buffer.append(",id="); // $NON-NLS-1$
      buffer.append(fRefactoringId);
      buffer.append(",description="); // $NON-NLS-1$
      buffer.append(fDescription);
      buffer.append(",project="); // $NON-NLS-1$
      buffer.append(fProject);
      buffer.append(",comment="); // $NON-NLS-1$
      buffer.append(fComment);
      buffer.append(",flags="); // $NON-NLS-1$
      buffer.append(fFlags);
      buffer.append("]"); // $NON-NLS-1$
    }

    return buffer.toString();
  }
}
